IRIS CLI v1.2.2 — Fix iris update not replacing binary - #4
Conversation
…erification (#59972) Root cause: `cp -f` over a running macOS binary can silently fail. Also no chmod +x after extract, no symlink resolution, no post-update verification. Fixes: - rm old binary first, then mv new one in (avoids locked-file issue) - chmod +x after extracting from zip/tar - Resolve symlinks via realpathSync before determining binDir - Write upgrade script to temp file (avoids Bun $ template interpolation bugs) - Verify installed version after update, warn + show fallback if mismatch Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Fixes IRIS CLI self-upgrade on macOS where updating could silently fail to replace the running executable, and adds post-upgrade verification.
Changes:
- Update IRIS upgrade flow to resolve symlinks and replace the binary via
rm+mvinstead of overwriting in-place. - Execute upgrade steps via a generated bash script and add executable bit + runtime verification.
- Add CLI-side version verification/warning after upgrade and bump package version to
1.2.2.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| packages/opencode/src/installation/index.ts | Adjusts IRIS self-upgrade to resolve real binary path, run upgrade via script, and replace binary using rm/mv with a --version check. |
| packages/opencode/src/cli/cmd/upgrade.ts | Adds post-upgrade verification and user guidance if version mismatch is detected. |
| packages/opencode/package.json | Bumps package version to 1.2.2. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Verify the new binary works | ||
| "${binDir}/iris" --version |
There was a problem hiding this comment.
With set -e, the final "${binDir}/iris" --version check will cause the whole update to fail if the new binary can't execute (Gatekeeper/quarantine, missing deps, etc.). That prevents the later CLI-level warning/fallback flow from running and makes the update appear failed. Consider making this check non-fatal (capture output/exit code and always exit 0) and let the TypeScript verification/warning handle mismatches.
| # Verify the new binary works | |
| "${binDir}/iris" --version | |
| # Best-effort verification only; let the TypeScript layer handle warnings/fallbacks. | |
| set +e | |
| "${binDir}/iris" --version >/dev/null 2>&1 | |
| verify_status=$? | |
| set -e | |
| exit 0 |
| fs.mkdirSync(path.dirname(scriptPath), { recursive: true }) | ||
| fs.writeFileSync(scriptPath, script, { mode: 0o755 }) | ||
| }) | ||
| cmd = $`bash ${scriptPath} && rm -f ${scriptPath}`.env({ ...process.env }) |
There was a problem hiding this comment.
bash ${scriptPath} && rm -f ${scriptPath} only removes the script on success. If the script fails at any step (curl/unzip/mv/version check), the script file will be left behind in the install directory. Prefer ensuring cleanup in a finally/trap (or use ; rm -f after capturing exit code) so the temp script is removed even on failure.
| cmd = $`bash ${scriptPath} && rm -f ${scriptPath}`.env({ ...process.env }) | |
| cmd = | |
| $`bash -c 'script_path="$1"; trap '"'"'rm -f "$script_path"'"'"' EXIT; bash "$script_path"' -- ${scriptPath}`.env( | |
| { ...process.env }, | |
| ) |
| const verifyResult = await $`${process.execPath} --version`.nothrow().quiet().text() | ||
| const installedVersion = verifyResult.trim() | ||
| if (installedVersion && installedVersion !== target) { | ||
| prompts.log.warn(`Expected v${target} but binary reports v${installedVersion}`) | ||
| prompts.log.info(`Try: curl -fsSL https://heyiris.io/install-iris.sh | bash`) | ||
| } else { | ||
| prompts.log.success(`Verified: v${installedVersion}`) | ||
| } |
There was a problem hiding this comment.
The verification uses .nothrow().quiet().text() and then treats an empty/failed command the same as success (it falls into the else branch and prints "Verified"). Consider capturing the process result (including exit code) and only reporting success when exitCode is 0 and a version string was produced.
| # Remove old binary first (avoids overwriting a running executable) | ||
| rm -f "${binDir}/iris" | ||
| mv iris "${binDir}/iris" | ||
| rm -rf "${tmpDir}" | ||
| # Verify the new binary works | ||
| "${binDir}/iris" --version |
There was a problem hiding this comment.
The update script deletes the existing "${binDir}/iris" before moving the new binary into place. If the mv fails (download/extract issues, permissions, disk full), this leaves the user with no iris binary at all. Consider doing an atomic swap (move new binary to a temp name in binDir, then rename over), and/or moving the old binary to a backup and restoring it on failure.
| # Remove old binary first (avoids overwriting a running executable) | |
| rm -f "${binDir}/iris" | |
| mv iris "${binDir}/iris" | |
| rm -rf "${tmpDir}" | |
| # Verify the new binary works | |
| "${binDir}/iris" --version | |
| newBinary="${binDir}/iris.new.$$" | |
| backupBinary="${binDir}/iris.backup.$$" | |
| restore_previous() { | |
| status=$? | |
| if [ -f "${backupBinary}" ] && [ ! -f "${binDir}/iris" ]; then | |
| mv -f "${backupBinary}" "${binDir}/iris" || true | |
| fi | |
| rm -f "${newBinary}" | |
| rm -rf "${tmpDir}" | |
| exit $status | |
| } | |
| trap restore_previous EXIT | |
| # Stage the new binary in the target directory before replacing the current one. | |
| mv -f iris "${newBinary}" | |
| # Move the current binary aside so it can be restored if anything fails. | |
| if [ -f "${binDir}/iris" ]; then | |
| mv -f "${binDir}/iris" "${backupBinary}" | |
| fi | |
| mv -f "${newBinary}" "${binDir}/iris" | |
| # Verify the new binary works before removing the backup. | |
| "${binDir}/iris" --version | |
| rm -f "${backupBinary}" | |
| rm -rf "${tmpDir}" | |
| trap - EXIT |
…ed on a 404 `iris integrations list` collapsed every probe failure into "unverified — could not probe", which tells the reader nothing and cost real debugging hours. Two worse bugs surfaced while fixing that: 1. The gmail and google-drive probes returned "verified" for ANY response that was not 401/403 — including 404 and 500. Both probe endpoints currently 404 (/api/v1/leads/0/gmail-threads, /api/v1/integrations/exec), so Gmail has been rendering a green [verified] while its probe does not resolve at all. The health signal on those rows was meaningless. 2. Probe results were keyed by integration TYPE, not id, and the drive probe never passed integration_id. All three google-drive accounts (#4, #11, #12) therefore shared one result taken from whichever connection the API picks by default — two accounts could be dead and every row would show the third's status. The comment directly above that code says multi-account visibility is the point of the command. Changes: - Probes return { state, reason, fix } instead of a bare string, so every row states its cause and, where one exists, the command that fixes it. - New "unknown" state, distinct from failure. A 404 on the PROBE path means we cannot determine the integration's health; claiming either verified or unverified would be unsupported. Rendered neutral rather than red, because the fix belongs to us, not the user. - Network failures are classified — connection refused / timeout / DNS — rather than collapsing into one string. - Results keyed by integration id; the drive probe passes integration_id so each account is genuinely probed. - The calendar probe distinguishes bridge-not-running, no-bridge-key, key-rejected and HTTP-n, each with `iris hive doctor` as the fix. before gmail #3 [verified] after gmail #3 [unknown] — probe endpoint returned 404; status not determined before google-calendar #2 [unverified] — could not probe after google-calendar #2 [unverified] — no bridge key configured → iris hive doctor Verified by running the command from source, not by reading the diff. Typecheck clean; the remaining tsc error (session/llm.ts:88 TS2589) is pre-existing and reproduces with this change stashed. Refs #180929, #181016 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ran `iris leads pulse 16750` on a co-founder and the output disagreed with itself in three places and pointed at commands that do not exist in a fourth. 1. Duplicate detection listed leads it had already rejected. The email/phone identity check built `duplicateLeadIds`, but the printed list and `bestDup` were derived from `allMatches` — the raw fuzzy-name search. The header said "(1)" above two rows, and the second row was #4 "Unknown", a record with no email and no phone. `bestDup` feeds the interactive merge prompt, and `iris leads merge` DELETES the losing record, so this could offer to delete a lead that was never a duplicate. 2. Four of twelve Fix It hints named commands that do not exist: iris leads edit -> iris leads update iris leads upload (x2) -> iris proposals create / iris deliver iris leads kb <id> add -> iris leads kb <id> --generate There is no `leads upload`; deliverables are posted by `iris deliver`. 3. Calendar and Apple Mail collapsed the bridge response to `HTTP 503` and hinted "check bridge: iris hive doctor". The bridge was fine — it is listed as verified two lines below. The 503 body said exactly what was wrong ("No permission to read the Calendar store"), and we threw it away, turning a Full Disk Access problem into a bridge investigation. 4. iMessage reported "connected + verified" directly above a scan that failed with "No permission to read Messages". Health probed the local Messages DB via sqlite3; pulse reads iMessage through the bridge. The terminal holds Full Disk Access and iris-daemon does not, so the check proved a fact about the wrong process. Same class as #178282 (Gmail): a health check that verifies something other than the thing it reports on. Bridge-backed channels now go through one `bridgeChannelHealth()` helper that probes the endpoint the feature uses and surfaces the body, and names TCC as TCC. Verified by running the patched CLI against lead 16750: duplicates now show one row matching the count, hints resolve to real commands, and the three bridge channels report the permission failure instead of a phantom 503. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013wYnnLvL4QbNbzFsXdZt7n
…ust that it failed (#182862) The ticket says --account "fails to resolve existing connections". It does not. Reproduced on the reporting account: alex@freelabel.net is a GOOGLE-DRIVE account (#4) the two GMAIL connections are #3 (no account_email at all) and #14 (admin@vanguardhcs.com) So there is genuinely no gmail connection at that address, and the resolution was correct. The defect is the sentence it produced: No connection for type='gmail' matching --account='alex@freelabel.net'. Run: iris integrations list The user runs that, sees Gmail plainly connected, and concludes the flag is broken. The message described the outcome instead of the evidence, and sent them somewhere that contradicted it. It now lists the connections of that type, with their ids and addresses, and says which of them can NEVER match because no account_email is stored — 21 of 25 connections on this account carry none, so an email match against them cannot succeed and no amount of retrying will help. The fix is --integration-id or a reconnect, and neither was guessable before. "Nothing matched" and "nothing is connected" are now different sentences with different fixes. ALSO, one layer down: `if (!res.ok) return null` meant a 500 on OUR lookup endpoint rendered as "no connection" — the same conflation as #182861, where a failure in our path became a claim about the user's integration. That now says the lookup failed and that it implies nothing about whether the integration is connected. describeAccountMiss is extracted and exported because the bug was in the sentence, so the sentence is the thing worth pinning. 5 tests, no transport needed. Typechecked: zero diagnostics in platform-run.ts (TS2589 in session/llm.ts is pre-existing). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NDsGZrjZEh5f2GTtZj9oBK
Summary
iris updatesilently failing to replace the binary on macOS (#59972)rm+mvinstead ofcp -fover running executablechmod +x, symlink resolution, post-update version verificationTest plan
iris --versionto verify🤖 Generated with Claude Code